package dk.javacode.srsm;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.log4j.Logger;

import dk.javacode.srsm.annotations.Column;
import dk.javacode.srsm.annotations.Table;
import dk.javacode.srsm.exceptions.MappingException;
import dk.javacode.srsm.util.ReflectionUtil;

public class TableDescriptor {

	private static Logger log = Logger.getLogger(TableDescriptor.class);

	private static ConcurrentMap<Class<?>, TableDescriptor> descriptors = new ConcurrentHashMap<Class<?>, TableDescriptor>();

	private static ReentrantLock buildDescriptorLock = new ReentrantLock();

	private String tableName;
	private Class<?> mappedClass;
	private Map<String, ColumnDescriptor> columns = new HashMap<String, ColumnDescriptor>();

	private Map<String, TableDescriptor> referencedTypes = new HashMap<String, TableDescriptor>();

	public String getTableName() {
		return tableName;
	}

	public void setTableName(String tableName) {
		this.tableName = tableName;
	}

	public Class<?> getMappedClass() {
		return mappedClass;
	}

	public void setMappedClass(Class<?> mappedClass) {
		this.mappedClass = mappedClass;
	}

	public ColumnDescriptor getColumnDescriptor(String name) {
		return columns.get(name);
	}

	public void addColumnDescriptor(ColumnDescriptor column) {
		columns.put(column.getColumnName(), column);
	}

	public void addReferencedType(TableDescriptor table) {
		referencedTypes.put(table.getTableName(), table);
	}

	public Map<String, TableDescriptor> getReferencedTypes() {
		return Collections.unmodifiableMap(referencedTypes);
	}

	public Collection<ColumnDescriptor> getColumnDescriptors() {
		return Collections.unmodifiableCollection(columns.values());
	}

	/**
	 * This method will build a table descriptor for the given class. The build
	 * method caches previously built descriptors in a map. If a cached instance
	 * is found, then this instance is returned instead.
	 * 
	 * @param clazz
	 * @return
	 * @throws MappingException
	 */
	public static <E> TableDescriptor build(Class<E> clazz) throws MappingException {
		// Lookup/create the tableDescriptor for this Class/Type
		TableDescriptor tableDescriptor = descriptors.get(clazz);
		if (tableDescriptor != null) {
			return tableDescriptor;
		}
		try {
			buildDescriptorLock.lock();
			tableDescriptor = descriptors.get(clazz);
			if (tableDescriptor == null) {
				tableDescriptor = TableDescriptor.buildNew(clazz);
				descriptors.put(clazz, tableDescriptor);
			}
			return tableDescriptor;
		} finally {
			buildDescriptorLock.unlock();
		}
	}

	public static <E> TableDescriptor buildNew(Class<E> clazz) throws MappingException {
		log.info("Building table descriptor for class: " + clazz.getCanonicalName());
		Table tableAnnotation = clazz.getAnnotation(Table.class);
		if (tableAnnotation == null) {
			throw new MappingException("Unable to map unannotated class: " + clazz.getName());
		}

		TableDescriptor table = new TableDescriptor();
		table.setMappedClass(clazz);
		table.setTableName(tableAnnotation.name());

		for (Field field : clazz.getDeclaredFields()) {
			Column columnAnnotation = field.getAnnotation(Column.class);
			if (columnAnnotation != null) {
				String columnName = columnAnnotation.name().toLowerCase();
				ColumnDescriptor column = new ColumnDescriptor();
				log.debug("Setting data-converter for column: " + columnName + " to "
						+ columnAnnotation.dataConverter().getSimpleName());
				column.setDataConverter(ReflectionUtil.getNewInstance(columnAnnotation.dataConverter()));
				column.setColumnName(columnName);
				column.setColumnField(field);
				Method setMethodForField = ReflectionUtil.getSetMethodForField(clazz, field);
				if (setMethodForField == null) {
					throw new MappingException("Unable to find setter method for field: " + field);
				}
				column.setColumnSetMethod(setMethodForField);
				if (!"".equals(columnAnnotation.columnReference())) {
					log.debug("Found reference '" + columnAnnotation.columnReference() + "' to class: "
							+ field.getType());
					Field referencedField = ReflectionUtil
							.getField(field.getType(), columnAnnotation.columnReference());
					if (referencedField == null) {
						throw new MappingException("Unable to find referenced field "
								+ columnAnnotation.columnReference());
					}
					column.setColumnReference(referencedField);

					Method referencedSetMethod = ReflectionUtil.getSetMethodForField(field.getType(), referencedField);
					if (referencedSetMethod == null) {
						throw new MappingException("Unable to find setter method for referenced field "
								+ referencedField);
					}
					column.setColumnReferenceSetter(referencedSetMethod);

					TableDescriptor referencedType = TableDescriptor.build(field.getType());
					table.addReferencedType(referencedType);
				}
				column.setNullable(columnAnnotation.nullable());
				column.setPrimaryKey(columnAnnotation.primaryKey());
				table.addColumnDescriptor(column);
			}
		}
		return table;
	}

}
